package com.example.Nurse_Notifier;

import android.app.Activity;
import android.util.Log;
import interfaces.HandleReceivedData;
import interfaces.SendPacket;

import java.io.IOException;
import java.io.Serializable;
import java.math.BigInteger;
import java.net.*;
import java.util.ArrayList;
import java.util.Arrays;

public class UdpServer implements Runnable, SendPacket, Serializable {

    //server constants
    public static final byte FIRST_UNIQUE = -66;
    public static final byte SECOND_UNIQUE = -28;
    public static final int READ = 2;
    public static final int READ_REQ = 3;
    public static final int WRITE = 4;
    public static final int WRITE_REQ = 5;
    public static final int BROADCAST = 6;
    public static final int BROADCAST_REQ = 7;
    public static final int ADD = 8;
    public static final int ADD_REQ = 9;
    public static final int REM = 10;
    public static final int REM_REQ = 11;
    public static final int LEN_LINE = 12;
    public static final int LEN_LINE_REQ = 13;
    public static final int ADD_OLOL = 14;
    public static final int ADD_OLOL_REQ = 15;
    public static final int REM_OLOL = 16;
    public static final int REM_OLOL_REQ = 17;
    public static final int SHARED_LINE_CNT = 18;
    public static final int SHARED_LINE_CNT_REQ = 19;

    public static final int LINE_BLOCKS_COUNT = 0;
    public static final int LINE_ADDRSS = 1;
    public static final int LINE_STATUS = 2;
    public static final int LINE_STATUS_CH_REQ = 3;
    public static final int LINE_ADC = 4;
    public static final int LINE_ADC_FIXED = 5;
    public static final int LINE_PING_COUNT = 6;
    public static final int LINE_BUTTON_STATUS = 7;
    public static final int LINE_OLOL_ADDRS = 9;
    public static final int DEVICE_OFF = 0;
    public static final int DEVICE_IDLE = 1;
    public static final int DEVICE_SCAN = 2;
    public static final int DEVICE_ALARM = 3;

    private int command;
    private int nline;
    private int blockNum;
    private int blockCnt;
    private byte[] data;

    private int outPort;
    private transient DatagramPacket outPacket;
    private transient DatagramPacket inPacket;
    private DatagramSocket socket;
    private byte[] outAddr;
    private final int PACK_LEN = 13;
    private final int IN_DATA_LEN = 2048;
    private final int SOCKET_TIMEOUT = 1500;
    private final int WAIT_TIMEOUT = 3000;
    private boolean isSended = false;
    private boolean isFail = false;
    private boolean isTransmitted = false;
    private byte[] source;
    private int sourceLen;
    private int loopReturnStatus = -1;
    Activity activity;

    private byte readData[] = new byte[PACK_LEN];
    private byte inData[] = new byte[IN_DATA_LEN];

    private transient ArrayList<HandleReceivedData> transmitData = new ArrayList<HandleReceivedData>();
    private boolean isRunning = true;

    String[] loopStatuses = {
            "Transmitted",
            "Fail",
            "Timeout"
    };

    public UdpServer(byte[] outAddr, int inPort, int outPort, Activity activity) throws SocketException {
        this.outAddr = outAddr;
        this.outPort = outPort;
        this.activity = activity;
        socket = new DatagramSocket(inPort);
        socket.setSoTimeout(SOCKET_TIMEOUT);
        socket.setReuseAddress(true);
        socket.setBroadcast(true);
        inPacket = new DatagramPacket(inData, IN_DATA_LEN);
    }

    public boolean isRunning() {
        return isRunning;
    }

    protected boolean valid_packet(DatagramPacket dp) {
        if (dp.getPort() != outPort) {
            Log.i("SK", "Invalid port");
            return false;
        }
        sourceLen = dp.getLength();
        source = new byte[sourceLen];
        if (source != null && sourceLen != 0) {
            System.arraycopy(dp.getData(), 0, source, 0, sourceLen);
            byte[] crc = getByteCRC(source);
            if (crc[0] != source[sourceLen - 2] && crc[1] != source[sourceLen - 1]) {
                Log.i("SK", "Invalid crc : crc [0] -> " + crc[0] + " ; crc [1] -> " + crc[1] + " || source data " +
                        Integer.toHexString(IntToByte.byte2Int(source[sourceLen - 2])) +
                        " |  " + Integer.toHexString(IntToByte.byte2Int(source[sourceLen - 1])) + " " +
                        "|| pack sourceLen -> " + sourceLen
                        + " || source sourceLen -> " + source.length);
                return false;
            }
        } else {
            Log.i("SK", " source is null , you fucked up! " + " is source is null -> " + (source == null) + " is len" +
                    "is null -> " + (sourceLen == 0) + " source len val -> " + sourceLen);
        }
        return true;
    }

    public void setTransmitted(boolean status) {
        isTransmitted = status;
    }

    public boolean getTransmitStatus() {
        return isTransmitted;
    }

    boolean isBroadcast = false;
    byte[] val = new byte[2];
    int i = 0;

    @Override
    public synchronized void run() {
        while (isRunning) {
            try {
                if (isSended) {
                    send();
                    isSended = false;
                }
                socket.receive(inPacket);
                if (valid_packet(inPacket)) {
                    if (checkPacketContent(source)) {
                        Log.i("SK", "Package has passed checking successfully. Content: " + Arrays.toString(source));
                        transmitToListeners(source,
                                sourceLen);
                        setTransmitted(true);
                    } else {
                        Log.i("SK", "Received broadcast pack!!!!" + Arrays.toString(source));
                        transmitToListeners(source,
                                sourceLen);
                        setTransmitted(false);
                        isBroadcast = true;
                    }
                } else {
                    isFail = true;
                    Log.i("SK", "invalid packet has been received. Content: " + Arrays.toString(source));
                    setTransmitted(false);
                }

                inPacket.setLength(IN_DATA_LEN);
            } catch (SocketTimeoutException ex) {
//                Log.i("SK", "SocketTimeoutException has happened " + ex.getMessage());
            } catch (IOException ex) {
                Log.i("SK_ERROR", "io exception occurred in udp server loop");
            }
        }
        socket.close();
    }

    public void transmitToListeners(byte[] data, int length) {
        for (int i = 0; i < transmitData.size(); i++) {
            transmitData.get(i).getData(data, length);
        }
    }

    public void addListener(HandleReceivedData listener) {
        transmitData.add(listener);
    }

    public void addCollection(ArrayList<HandleReceivedData> collection) {
        transmitData.addAll(collection);
    }

    public void clearListenersList() {
        transmitData.clear();
    }

    public byte[] getByteCRC(byte[] data) {
        int buf = 0;
        short res = 0;
        byte bres[] = {-1, -1};
        int curval = 0;
        int len = data.length;
        if (data[0] == FIRST_UNIQUE
                && data[1] == SECOND_UNIQUE) {
            boolean isOdd = len % 2 > 0;

            buf = (IntToByte.byte2Int(data[2])) | (data[3] << 8);

            for (int i = 4; i < len - 4; i += 2) {
                curval = (IntToByte.byte2Int(data[i])) | (data[i + 1] << 8);
                buf ^= curval;
            }
            if (isOdd) {
                buf ^= data[len - 3];
            } else {
                buf ^= (IntToByte.byte2Int(data[len - 4])) | (data[len - 3] << 8);
            }
            res = (short) (0xffff - (buf & 0xffff));
            bres[0] = (byte) (res & 0x00ff);
            bres[1] = (byte) ((res & 0xff00) >> 8);
            return bres;
        } else {
            return bres;
        }
    }

    public byte[] customRequest(int command, int nline, int blockNum, int blockCnt, byte[] data) {
        setSendData(command, nline, blockNum, blockCnt, data);
        forceSend();
        timeoutWait();
        byte[] datas = null;
        if (isTransmitted || isBroadcast()) {
            if (source.length > 13) {
                datas = new byte[source.length - 13];
                System.arraycopy(source, 11, datas, 0, datas.length);
            }
            isTransmitted = false;
        }
        return datas;
    }

    public void setSendData(int command, int nline, int blockNum, int blockCnt, byte[] data) {
        this.command = command;
        this.nline = nline;
        this.blockNum = blockNum;
        this.blockCnt = blockCnt;
        this.data = data;
    }

    public void forceSend() {
        isSended = true;
    }

    @Override
    public void send() {
        String status = "";
        try {
            byte[] writePack;
            switch (command) {
                case READ: {
                    outPacket = new DatagramPacket(generateReadPack(command, nline, blockNum, blockCnt), PACK_LEN);
                    status = "READ";
                }
                break;

                case READ_REQ: {
                    outPacket = new DatagramPacket(generateReadPack(command, nline, blockNum, blockCnt), PACK_LEN);
                    status = "READ REQUEST";
                }
                break;

                case WRITE: {
                    writePack = generateWritePack(command, nline, blockNum, blockCnt, data);
                    outPacket = new DatagramPacket(writePack, writePack.length);
                    status = "WRITE";
                }
                break;

                case WRITE_REQ: {
                    writePack = generateWritePack(command, nline, blockNum, blockCnt, data);
                    outPacket = new DatagramPacket(writePack, writePack.length);
                    status = "WRITE REQUEST";
                }
                break;

                case BROADCAST: {
                    outPacket = new DatagramPacket(generateReadPack(command, nline, blockNum, blockCnt), PACK_LEN);
                    status = "BROADCAST REQUEST";
                }
                break;

                case ADD: {
                    outPacket = new DatagramPacket(generateReadPack(command, nline, blockNum, blockCnt), PACK_LEN);
                    status = "ADD DATA";
                }
                break;

                case REM: {
                    outPacket = new DatagramPacket(generateReadPack(command, nline, blockNum, blockCnt), PACK_LEN);
                    status = "REMOVE DATA";
                }
                break;

                case ADD_OLOL: {
                    outPacket = new DatagramPacket(generateReadPack(command, nline, blockNum, blockCnt), PACK_LEN);
                    status = "REGISTER OLOL DEVICE";
                }
                break;

                case ADD_OLOL_REQ: {
                    outPacket = new DatagramPacket(generateReadPack(command, nline, blockNum, blockCnt), PACK_LEN);
                    status = "REGISTER OLOL DEVICE REQUEST";
                }
                break;

                case REM_OLOL: {
                    outPacket = new DatagramPacket(generateReadPack(command, nline, blockNum, blockCnt), PACK_LEN);
                    status = "REMOVE OLOL DEVICE";
                }

                default:
                    break;
            }
            outPacket.setAddress(InetAddress.getByAddress(outAddr));
            outPacket.setPort(outPort);
            if (outPacket != null && socket != null) {
                socket.send(outPacket);
//                Log.i("SK", "Data has been sent! packet len ->" + Arrays.toString(outPacket.getData()) + " Status : " + status);
            } else {
                Log.i("SK", "Some of your objects is null. " + " Outpacket " + (outPacket == null) + " socket " +
                        (socket == null));
            }
            isSended = true;
        } catch (IOException e) {
            Log.i("SK", "Error while sending" + e.getCause());
        }
    }

    public byte[] generateWritePack(int command, int nline, int blockNum, int blockCnt, byte[] writePack) {
        byte[] result = new byte[13 + writePack.length];
        result[0] = FIRST_UNIQUE;
        result[1] = SECOND_UNIQUE;
        result[2] = 0;
        result[3] = (byte) (7 + writePack.length);
        result[4] = (byte) command;
        result[5] = (byte) nline;
        result[6] = 0;
        result[7] = (byte) blockNum;
        result[8] = 0;
        result[9] = (byte) blockCnt;
        result[10] = 0;
        System.arraycopy(writePack, 0, result, 11, writePack.length);
        byte crc[] = getByteCRC(result);
        result[result.length - 2] = crc[0];
        result[result.length - 1] = crc[1];
        return result;
    }

    public byte[] generateReadPack(int command, int nline, int blockNum, int blockCnt) {
        readData[0] = FIRST_UNIQUE;
        readData[1] = SECOND_UNIQUE;
        readData[2] = 0;
        readData[3] = 7;
        readData[4] = (byte) command;
        readData[5] = (byte) nline;
        readData[6] = 0;
        readData[7] = (byte) blockNum;
        readData[8] = 0;
        readData[9] = (byte) blockCnt;
        readData[10] = 0;
        byte crc[] = getByteCRC(readData);
        readData[11] = crc[0];
        readData[12] = crc[1];
        return readData;
    }

    public int getADCVal(int deviceNum) {
        Log.i("SK", "Get adc val");
        int resVal = -1;
        setSendData(READ, LINE_ADC, deviceNum, 1, null);
        forceSend();
        timeoutWait();
        if (isTransmitted) {
            resVal = (IntToByte.byte2Int(source[sourceLen - 4]) << 8) | IntToByte.byte2Int(source[sourceLen - 3]);
            isTransmitted = false;
        }
        return resVal;
    }

    public boolean isBroadcast() {
        return isBroadcast;
    }

    public int getDevicesCnt(boolean broadcast) {
        Log.i("SK", "Get devices count");
        int resVal = -1;
        setSendData(broadcast ? BROADCAST : READ, LINE_BLOCKS_COUNT, 0, 1, null);
        forceSend();
        timeoutWait();
        if (isTransmitted || isBroadcast()) {
            if (source != null) {
                resVal = source[sourceLen - 3] << 8 | source[sourceLen - 4];
                isTransmitted = false;
                isBroadcast = false;
            }
        }
        return resVal;
    }

    public boolean setDeviceStatus(int deviceNum, int status) {
        Log.i("SK", "Set device status");
        boolean returnStatus = false;
        data = new byte[1];
        data[0] = (byte) status;
        setSendData(WRITE, LINE_STATUS_CH_REQ, deviceNum, 1, data);
        forceSend();
        timeoutWait();
        if (isTransmitted) {
            if (source[sourceLen - 3] == status) {
                returnStatus = true;
                isTransmitted = false;
            }
        }
        return returnStatus;
    }

    public int getDeviceStatus(int deviceNum) {
        Log.i("SK", "Get device status");
        int resVal = -1;
        setSendData(READ, LINE_STATUS, deviceNum, 1, null);
        forceSend();
        timeoutWait();
        if (isTransmitted) {
            resVal = source[sourceLen - 3];
            isTransmitted = false;
        }
        return resVal;
    }

    public String getDeviceMacAddress(int deviceNum) {
        Log.i("SK", "Get device mac address");
        byte[] mac = new byte[4];
        setSendData(READ, LINE_ADDRSS, deviceNum, 1, null);
        forceSend();
        timeoutWait();
        if (isTransmitted) {
            mac[0] = source[sourceLen - 6];
            mac[1] = source[sourceLen - 5];
            mac[2] = source[sourceLen - 4];
            mac[3] = source[sourceLen - 3];
            isTransmitted = false;
        }
        return new BigInteger(1, mac).toString(16);
    }

    public boolean setButtonStatus(int devNum, boolean status) {
        Log.i("SK", "Set button status");
        byte[] send = new byte[1];
        boolean returnStatus = false;
        send[0] = status ? (byte) 1 : (byte) 0;
        setSendData(WRITE, LINE_BUTTON_STATUS, devNum, 1, send);
        forceSend();
        timeoutWait();
        if (isTransmitted) {
            if (send[0] == source[sourceLen - 3]) returnStatus = true;
        }
        return returnStatus;
    }

    public String getStatusById(int id) {
        switch (id) {
            case DEVICE_OFF: {
                return "Выключено";
            }
            case DEVICE_IDLE: {
                return "Простаивает";
            }
            case DEVICE_SCAN: {
                return "Сканирование";
            }
            case DEVICE_ALARM: {
                return "Тревога";
            }
            default:
                return "Неизвестно";
        }
    }

    public int timeoutWait() {
        boolean isTimeout = false;
        double start = System.currentTimeMillis();
        while (!isTransmitted && !isFail && !isTimeout) {
            if (System.currentTimeMillis() - start > WAIT_TIMEOUT) {
                isTimeout = true;
                loopReturnStatus = 2;
            }
        }
        if (isTransmitted) loopReturnStatus = 0;
        if (isFail) loopReturnStatus = 1;
        Log.i("SK", "Timeout status -> " + loopStatuses[loopReturnStatus]);
        return loopReturnStatus;
    }

    public boolean checkPacketContent(byte[] data) {
        return (getLineInPack(data) == nline &&
                getBlockInPack(data) == blockNum &&
                getCmdInPack(data) == command &&
                getCntBlockInPack(data) == blockCnt);
    }

    public static int getLineInPack(byte[] data) {
        if (data.length > 6) {
            return ((data[6] & 0xff) << 8) | (data[5] & 0xff);
        } else {
            return -1;
        }
    }

    public static int getBlockInPack(byte[] data) {
        if (data.length > 8) {
            return ((data[8] & 0xff) << 8) | (data[7] & 0xff);
        } else {
            return -1;
        }
    }

    public static int getCntBlockInPack(byte[] data) {
        if (data.length > 10) {
            return ((data[10] & 0xff) << 8) | (data[9] & 0xff);
        } else {
            return -1;
        }
    }

    public static int getCmdInPack(byte[] data) {
        if (data.length > 4) {
            return data[4] - 1;
        } else {
            return -1;
        }
    }

    public int[] getDevicesStatutes(int devsCnt) {
        if (devsCnt > 0) {
            Log.i("SK", "Getting devices' statuses");
            int[] results = new int[devsCnt];
            setSendData(READ, LINE_STATUS, 0, devsCnt, null);
            forceSend();
            timeoutWait();
            if (isTransmitted) {
                for (int i = 0; i < devsCnt; i++) {
                    results[i] = source[11 + i];
                }
                isTransmitted = false;
            }
            return results;
        } else return null;
    }

    public static int getDataLength(byte[] data) {
        return data.length - 13;
    }

    public String[] getDevicesMac(int devsCnt) {
        if (devsCnt > 0) {
            Log.i("SK", "Getting devices' macs");
            String[] results = new String[devsCnt];
            byte[] mac = new byte[4];
            setSendData(READ, LINE_ADDRSS, 0, devsCnt, null);
            forceSend();
            timeoutWait();
            if (isTransmitted) {
                for (int i = 0, j = 11; i < devsCnt; i++) {
                    mac[0] = source[j++];
                    mac[1] = source[j++];
                    mac[2] = source[j++];
                    mac[3] = source[j++];
                    results[i] = new BigInteger(1, mac).toString(16);
                }
                isTransmitted = false;
            }
            return results;
        } else return null;
    }
}